package arkham.data;

import java.util.*;
import java.io.*;

import com.twmacinta.util.MD5;
import com.twmacinta.util.MD5InputStream;

import arkham.util.AHLogger;

/**
 * The game loader class handles the game loading code including verifying
 * file integrity and ensuring that all required files are loaded.
 * @author ctheng
 *
 */
public class GameLoader
{
	private HashMap<Character, String> expansionSymbolMap;
	private HashMap<String, List<String>> expansionFileLists;
	private HashMap<String, List<String>> addedCards;
	private HashMap<String, List<String>> removedCards;
	private String mainBoardFile;
	private ArrayList<String> expansionsInPlay, missingFiles;
	private HashMap<String, String> md5Map;
	private HashMap<String, String> xmlFiles;
	private String md5Info, masterData;
	private boolean md5Verified;
	private GameData gameData;
	
	public GameLoader()
	{
		expansionSymbolMap = new HashMap<Character, String>();
		expansionFileLists = new HashMap<String, List<String>>();
		addedCards = new HashMap<String, List<String>>();
		removedCards = new HashMap<String, List<String>>();
		md5Map = new HashMap<String, String>();
		xmlFiles = new HashMap<String, String>();
		expansionsInPlay = new ArrayList<String>();
		missingFiles = new ArrayList<String>();
		md5Verified = false;
	}
	
	/**
	 * Loads the master data file containing names of files
	 * @param filename the filename of the master file
	 * @return true on a success, false on a failure
	 */
	public boolean loadMasterDataFromFile(String filename)
	{
		StringBuffer masterDataBuf = new StringBuffer();
		try
		{
			String mainSetName = "";
			Scanner sc = new Scanner(new File(filename));
			while (sc.hasNextLine())
			{
				String line = sc.nextLine();
				if (line.startsWith("//")) continue;
				masterDataBuf.append(line + "\n");
				if (line.equals("") || line.equals("--End")) break;
				if (line.startsWith("--"))
				{
					String expName = line.substring(2);
					if (expName.equals("Master"))
					{
						while (true)
						{
							line = sc.nextLine();
							if (line.startsWith("//")) continue;
							masterDataBuf.append(line + "\n");
							if (line.equals("--")) break;
							String[] tokens = line.split(",");
							if (tokens.length != 2) throw new IOException("Malformed input file.");
							if (tokens[0].length() != 1) 
								throw new IOException("May only have one symbol per expansion.");
							char expSymbol = tokens[0].charAt(0);
							if (expSymbol == '*' || 
								Character.isLowerCase(expSymbol) || 
								Character.isDigit(expSymbol))
							{
								if (expansionSymbolMap.containsKey(expSymbol) ||
										expansionFileLists.containsKey(tokens[1]))
								{
									throw new IOException("Duplicate expansion or symbol detected.");
								}
								expansionSymbolMap.put(expSymbol, tokens[1]);
								expansionFileLists.put(tokens[1], new ArrayList<String>());
								if (expSymbol == '*')
								{
									mainSetName = tokens[1];
								}
							}
							else
							{
								throw new IOException("Invalid expansion symbol for " + tokens[1] + ".");
							}
						}
					}
					else
					{
						if (expansionFileLists.containsKey(expName))
						{
							List<String> list = expansionFileLists.get(expName);
							while (sc.hasNextLine())
							{
								line = sc.nextLine();
								if (line.startsWith("//")) continue;
								masterDataBuf.append(line + "\n");
								if (line.equals("--")) break;
								if (line.startsWith(">>"))
								{
									list.add(line.substring(2));
								}
							}
							if (list.size() != 7)
							{
								throw new IOException("Wrong number of input files specified for " + expName);
							}
							if (expName.equals(mainSetName))
							{
								mainBoardFile = list.get(4);
							}
						}
						else
						{
							throw new IOException("Invalid expansion definition found: " + expName);
						}
					}
				}
			}
			masterData = masterDataBuf.toString();
			return true;
		}
		catch (IOException e)
		{
			AHLogger.error(e.toString());
			return false;
		}
	}
	
	/**
	 * Loads the master data file containing names of files from a string
	 * @param text the text of the master file received from the server
	 * @return true on a success, false on a failure
	 */
	public boolean loadMasterDataFromText(String text)
	{
		StringBuffer masterDataBuf = new StringBuffer();
		try
		{
			String mainSetName = "";
			Scanner sc = new Scanner(text);
			while (sc.hasNextLine())
			{
				String line = sc.nextLine();
				if (line.startsWith("//")) continue;
				masterDataBuf.append(line + "\n");
				if (line.equals("") || line.equals("--End")) break;
				if (line.startsWith("--"))
				{
					String expName = line.substring(2);
					if (expName.equals("Master"))
					{
						while (true)
						{
							line = sc.nextLine();
							if (line.startsWith("//")) continue;
							masterDataBuf.append(line + "\n");
							if (line.equals("--")) break;
							String[] tokens = line.split(",");
							if (tokens.length != 2) throw new IOException("Malformed input file.");
							if (tokens[0].length() != 1) 
								throw new IOException("May only have one symbol per expansion.");
							char expSymbol = tokens[0].charAt(0);
							if (expSymbol == '*' || 
								Character.isLowerCase(expSymbol) || 
								Character.isDigit(expSymbol))
							{
								if (expansionSymbolMap.containsKey(expSymbol) ||
										expansionFileLists.containsKey(tokens[1]))
								{
									throw new IOException("Duplicate expansion or symbol detected.");
								}
								expansionSymbolMap.put(expSymbol, tokens[1]);
								expansionFileLists.put(tokens[1], new ArrayList<String>());
								if (expSymbol == '*')
								{
									mainSetName = tokens[1];
								}
							}
							else
							{
								throw new IOException("Invalid expansion symbol for " + tokens[1] + ".");
							}
						}
					}
					else
					{
						if (expansionFileLists.containsKey(expName))
						{
							List<String> list = expansionFileLists.get(expName);
							while (sc.hasNextLine())
							{
								line = sc.nextLine();
								if (line.startsWith("//")) continue;
								masterDataBuf.append(line + "\n");
								if (line.equals("--")) break;
								if (line.startsWith(">>"))
								{
									list.add(line.substring(2));
								}
							}
							if (list.size() != 7)
							{
								throw new IOException("Wrong number of input files specified for " + expName);
							}
							if (expName.equals(mainSetName))
							{
								mainBoardFile = list.get(4);
							}
						}
						else
						{
							throw new IOException("Invalid expansion definition found: " + expName);
						}
					}
				}
			}
			masterData = masterDataBuf.toString();
			return true;
		}
		catch (IOException e)
		{
			AHLogger.error(e.toString());
			return false;
		}
	}
	
	/**
	 * Loads the XML files as specified in the load string. Prior to invoking
	 * this method, the Master data must be loaded.
	 * @param loadString The load string
	 */
	public boolean loadAsServer(String loadString)
	{
		Scanner sc = new Scanner(loadString);
		String setsToLoad = sc.nextLine();
		boolean mainSetLoaded = false;
		StringBuffer md5buf = new StringBuffer();
		for (int i = 0; i < setsToLoad.length(); i++)
		{
			char expSymbol = setsToLoad.charAt(i);
			if (expSymbol == '*') 
			{
				mainSetLoaded = true;
			}
			if (!expansionSymbolMap.containsKey(expSymbol))
			{
				AHLogger.warning("Cannot find expansion symbol " + expSymbol);
				return false;
			}
			String expName = expansionSymbolMap.get(expSymbol);
			addedCards.put(expName, new ArrayList<String>());
			removedCards.put(expName, new ArrayList<String>());
			expansionsInPlay.add(expName);
			List<String> expFiles = expansionFileLists.get(expName);
			for (int j = 0; j < expFiles.size(); j++)
			{
				if (expFiles.get(j).equals("*NONE*"))
				{
					continue;
				}
				try
				{
					MD5InputStream md5in = new MD5InputStream(new FileInputStream(expFiles.get(j)));
					StringBuffer sb = new StringBuffer();
					while (true)
					{
						int val = md5in.read();
						if (val == -1) break;
						sb.append((char)val);
					}
					xmlFiles.put(expFiles.get(j), sb.toString());
					MD5 md5 = md5in.getMD5();
					String md5val = md5.asHex();
					md5Map.put(expFiles.get(j), md5val);
					md5buf.append(expFiles.get(j) + "," + md5val + "\n");
					md5in.close();
				}
				catch (IOException e)
				{
					AHLogger.error(e.toString());
					return false;
				}
			}
		}
		md5Info = md5buf.toString();
		while (sc.hasNextLine())
		{
			String line = sc.nextLine();
			if (line.startsWith("//")) continue;
			if (line.equals("--End")) break;
			if (line.length() != 2) 
			{
				AHLogger.error("GameLoader", "Malformed load string.");
				return false;
			}
			char q = line.charAt(0);
			char expSymbol = line.charAt(1);
			String expName = expansionSymbolMap.get(expSymbol);
			if (!addedCards.containsKey(expName))
			{
				AHLogger.error("GameLoader", "Only cards in added sets may be modified");
				return false;
			}
			if (q == '+')
			{
				while (sc.hasNextLine())
				{
					line = sc.nextLine();
					if (line.equals(q + "" + expSymbol)) break;
					addedCards.get(expName).add(line);
				}
			}
			else if (q == '-')
			{
				while (sc.hasNextLine())
				{
					line = sc.nextLine();
					if (line.equals(q + "" + expSymbol)) break;
					removedCards.get(expName).add(line);
				}
			}
			else
			{
				AHLogger.error("GameLoader", "Malformed load string");
				return false;
			}
		}
		
		if (!mainSetLoaded)
		{
			AHLogger.error("Must load base set");
			return false;
		}
		
		return true;
	}
	
	/**
	 * Loads the XML files as specified in the load string. Prior to invoking
	 * this method, the Master data must be loaded.
	 * @param loadString The load string
	 */
	public boolean loadAsClient(String loadString)
	{
		Scanner sc = new Scanner(loadString);
		String setsToLoad = sc.nextLine();
		boolean mainSetLoaded = false;
		for (int i = 0; i < setsToLoad.length(); i++)
		{
			char expSymbol = setsToLoad.charAt(i);
			if (expSymbol == '*') 
			{
				mainSetLoaded = true;
			}
			if (!expansionSymbolMap.containsKey(expSymbol))
			{
				AHLogger.warning("Cannot find expansion symbol " + expSymbol);
				return false;
			}
			String expName = expansionSymbolMap.get(expSymbol);
			addedCards.put(expName, new ArrayList<String>());
			removedCards.put(expName, new ArrayList<String>());
			expansionsInPlay.add(expName);
			List<String> expFiles = expansionFileLists.get(expName);
			for (int j = 0; j < expFiles.size(); j++)
			{
				if (expFiles.get(j).equals("*NONE*"))
				{
					continue;
				}
				try
				{
					File f = new File(expFiles.get(j));
					if (!f.exists())
					{
						continue;
					}
					MD5InputStream md5in = new MD5InputStream(new FileInputStream(f));
					StringBuffer sb = new StringBuffer();
					while (true)
					{
						int val = md5in.read();
						if (val == -1) break;
						sb.append((char)val);
					}
					xmlFiles.put(expFiles.get(j), sb.toString());
					MD5 md5 = md5in.getMD5();
					String md5val = md5.asHex();
					md5Map.put(expFiles.get(j), md5val);
					md5in.close();
				}
				catch (IOException e)
				{
					AHLogger.warning("GameLoader.loadAsClient", e.toString());
					continue;
				}
			}
		}
		while (sc.hasNextLine())
		{
			String line = sc.nextLine();
			if (line.startsWith("//")) continue;
			if (line.equals("--End")) break;
			if (line.length() != 2) 
			{
				AHLogger.error("GameLoader", "Malformed load string.");
				return false;
			}
			char q = line.charAt(0);
			char expSymbol = line.charAt(1);
			String expName = expansionSymbolMap.get(expSymbol);
			if (!addedCards.containsKey(expName))
			{
				AHLogger.error("GameLoader", "Only cards in added sets may be modified");
				return false;
			}
			if (q == '+')
			{
				while (sc.hasNextLine())
				{
					line = sc.nextLine();
					if (line.equals(q + "" + expSymbol)) break;
					addedCards.get(expName).add(line);
				}
			}
			else if (q == '-')
			{
				while (sc.hasNextLine())
				{
					line = sc.nextLine();
					if (line.equals(q + "" + expSymbol)) break;
					removedCards.get(expName).add(line);
				}
			}
			else
			{
				AHLogger.error("GameLoader", "Malformed load string");
				return false;
			}
		}
		
		if (!mainSetLoaded)
		{
			AHLogger.error("Must load base set");
			return false;
		}
		
		return true;
	}
	
	/**
	 * The client calls this method when it receives a missing file from the
	 * server.
	 * @param filename the file name
	 * @param filedata the file data
	 * @return true if the file is successfully received
	 */
	public boolean addFile(String filename, String filedata)
	{
		synchronized(missingFiles)
		{
			int index = missingFiles.indexOf(filename);
			if (index == -1) return false;
			xmlFiles.put(filename, filedata);
			missingFiles.remove(index);
			return true;
		}
	}
	
	/**
	 * This method is called by the client to compare whether the loaded data files
	 * are identical to those used by the server
	 * @param md5 the MD5 info string
	 * @return true if the comparison is successful, whether or not there are missing
	 * files. Missing files are stored in a separate list.
	 */
	public boolean compareMD5 (String md5)
	{
		if (md5Verified)
		{
			return true;
		}
		Scanner sc = new Scanner(md5);
		synchronized(missingFiles)
		{
			while (sc.hasNextLine())
			{
				String line = sc.nextLine();
				if (line.equals("")) break;
				String[] tokens = line.split(",");
				if (tokens.length != 2)
				{
					AHLogger.error("GameLoader.compareMD5", "Malformed md5 info string.");
					return false;
				}
				String filename = tokens[0];
				if (md5Map.containsKey(filename))
				{
					if (!md5Map.get(filename).equals(tokens[1]))
					{
						missingFiles.add(filename);
					}
				}
				else
				{
					missingFiles.add(filename);
				}
			}
		}
		md5Verified = true;
		return true;
	}
	
	/**
	 * Gets the list of expansions in play
	 * @return the list of expansions in play
	 */
	public List<String> getExpansionsInPlay()
	{
		return expansionsInPlay;
	}
	
	/**
	 * Gets an MD5 info string to send to the client
	 * @return the md5 info string
	 */
	public String getMD5Info()
	{
		return md5Info;
	}
	
	/**
	 * Gets the master data to send to the client
	 * @return the master data
	 */
	public String getMasterData()
	{
		return masterData;
	}
	
	/**
	 * Gets the list of files that the client is missing
	 * @return the list of missing files
	 */
	public List<String> getMissingFiles()
	{
		return Collections.unmodifiableList(missingFiles);
	}
	
	/**
	 * Gets the number of missing files
	 * @return the number of missing files
	 */
	public int getNumMissingFiles()
	{
		return missingFiles.size();
	}
	
	/**
	 * Gets the file specified by filename. This method is 
	 * called by the server to transmit info to a client
	 * @param filename the file name
	 * @return the file data
	 */
	public String getFile(String filename)
	{
		return xmlFiles.get(filename);
	}
	
	/**
	 * Loads the game data with the XML strings
	 * @param numPlayers the number of players
	 * @return the game data
	 */
	public GameData loadGameData(int numPlayers)
	{
		if (gameData != null && gameData.isLoaded())
		{
			return gameData;
		}
		gameData = new GameData(numPlayers);
		ArrayList<String>[] lists = (ArrayList<String>[])new ArrayList[7];
		for (int i = 0; i < 7; i++)
		{
			lists[i] = new ArrayList<String>();
		}
		
		for (int i = 0; i < expansionsInPlay.size(); i++)
		{
			String expName = expansionsInPlay.get(i);
			List<String> filenames = expansionFileLists.get(expName);
			for (int j = 0; j < 7; j++)
			{
				if (filenames.get(j).equals("*NONE*"))
				{
					continue;
				}
				if (j == 4 && filenames.get(j).equals(mainBoardFile))
				{
					continue;
				}
				String xml = xmlFiles.get(filenames.get(j));
				if (xml == null)
				{
					AHLogger.error("GameLoader.loadGameData", "Missing file: " + filenames.get(j));
					return null;
				}
				lists[j].add(xml);
			}
		}
		// TODO: code in ignored/added cards
		gameData.loadInvCardsFromText(lists[0]);
		gameData.loadInvestigatorsFromText(lists[1]);
		gameData.loadMonstersFromText(lists[2]);
		gameData.loadAncientOnesFromText(lists[3]);
		gameData.loadBoardsFromText(xmlFiles.get(mainBoardFile), lists[4]);
		gameData.loadResourcesFromText(lists[5]);
		gameData.loadEncountersFromText(lists[6]);
		boolean res = gameData.validateLoadedData();
		if (res)
		{
			return gameData;
		}
		else
		{
			return null;
		}
	}
}